本次环境为直接引用 Vue.js(开发版) , 没有使用脚手架

Vue 的核心思想是数据驱动 DOM,也建议我们避免直接去进行 DOM 操作,但是在很多业务里,避免不了去使用一些第三方的库,比如 popper.js、swiper.js 等,这些基于原生 JavaScript 的库都有创建和更新及销毁的完整生命周期,如果与 Vue 搭配使用的话,不可避免的会出现直接操作 DOM 的现象,这个时候需要合理的利用一些 Vue 的机制 : $nextTick

使用场景

先来看一个简单的demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
<div id="div" v-if="showDiv">这是一段文本</div>
<button @click="getText">获取Div内容</button>
</div>

<script>
var app=new Vue({
el:"#app",
data:{
showDiv:false
},
methods:{
getText:function(){
this.showDiv=true;
var text=document.getElementById('div').innerHTML;
console.log(text);
}
}
})
</script>

很简单的一段代码:有一个 div ,默认使用 v-if 进行隐藏,点击按钮后,改变 v-if的值,将 div 的值显示出来,同时拿到这个值,输出到控制台上。

按照正常的思路,如果 v-if 的值是 false,直接去获取 div 的内容是获取不到的,因为此时 div 还未被创建,那么应该在点击按钮后,改变 v-if 的值为 true ,div 才会被创建,此时才能被获取到。

但是按照这种思路,控制台会抛出一个异常: Cannot read property 'innerHTML' of null

意思是获取不到 div 元素。

异步更新队列

按我们正常的思路来说上述代码应该是可以获取到 div 的,但是在实际运行过程中并未能获取到,这里面就涉及到 Vue 一个重要的概念:异步更新队列

Vue 在观察到数据变化时,并不是直接更新 DOM ,而是开启一个队列,并缓冲在同一事件循环过程中发生的所有数据改变,在缓冲时会去除重复数据,从而避免不必要的计算和 DOM 操作。然后,在下一个事件循环 tick 中,Vue 刷新队列并执行实际(已去重的)工作。举个例子来说,如果使用 for 循环来动态改变数据 100 次,其实 Vue 只会应用最后一次改变,如果没有这种机制的话,DOM 就要重绘 100 次,这是一个很大的开销。

关于队列,Vue 会根据当前浏览器环境优先使用 Promise.then 和 MutationObserver ,如果都不支持,就会使用 setTimeout 代替。

解决方法

知道 Vue 异步更新 DOM 的原理后,上面的报错应该就很容易理解了。在执行 this.showDiv=true; 时,div 仍然没有被创建出来,直到下一个 Vue 事件循环时,才开始创建。

如果引用了其他的第三方库,需要立即创建、立即获取,这个时候就需要避开 Vue 的这种机制了,$nextTick 就是用来指导什么时候 DOM 更新完成的,所以上面的代码需要进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="app">
<div id="div" v-if="showDiv">这是一段文本</div>
<button @click="getText">获取Div内容</button>
</div>

<script>
var app=new Vue({
el:"#app",
data:{
showDiv:false
},
methods:{
getText:function(){
this.showDiv=true;
//update begin
this.$nextTick(function(){
var text=document.getElementById('div').innerHTML;
console.log(text);
})
//update end
}
}
})
</script>

这时再点击按钮,控制台就会打印出 div 的内容:这是一段文本